/* 
 * File:    Client for "Tiny World of Warcraft" (TWW)
 * Authors: Jonathan Kotker (ee122-bw) and Shaan Mulchandani (ee122-bv)
 */

#define MIN(a, b) ((a) > (b) ? (b) : (a))
#define MAX(a, b) ((a) < (b) ? (b) : (a))

#include "constants.h"
#include "fileio.h"
#include "messages.h"
#include "packetAssembler.h"
#include "packetContents.h"
#include "packetParser.h"
#include "playerList.h"
#include "playerStatistics.h"
#include "playerListNode.h"
#include "stringUtils.h"
#include "UDP_Assembler.h"
#include "UDP_Contents.h"
#include "UDP_Parser.h"

#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

void srand(unsigned int seed);

/*
 * Tasks to be run before exiting the program. `playerList' is the list
 * of current players, and `status' is the reason the program is exiting.
 */
void done(playerList* currentPlayers, int status) {
    clearList(currentPlayers);
    exit(status);
};

/*
 * Sends the data in the `bytes' array. This method keeps sending until
 * all of the data has been sent, as the `send' method, by itself, does
 * not send all of the data in one go.
 */
void sendBytes(int socketFd, char *bytes, int len) {
    int bytesSent = 0, total = 0, bytesLeft = len;

    while (total < len) {
        bytesSent = send(socketFd, bytes + total, bytesLeft, 0);
        if (bytesSent == -1) {
            // Connection to server is broken.
            close(socketFd);
            on_disconnection_from_server();
            exit(EXIT_FAILURE);
        }
        total += bytesSent;
        bytesLeft -= bytesSent;
    }
};

/*
 * Returns a 1 if the `other' player is visible to you, based on
 * your current position; 0 otherwise.
 */
int withinRange(playerStatistics* self, playerStatistics* other) {
    if (self == NULL || other == NULL) return NULL;

    int playerXLoc = other->xLoc;
    int playerYLoc = other->yLoc;
    int currXLoc = self->xLoc;
    int currYLoc = self->yLoc;

    int currXMin = MAX(0, currXLoc - 5);
    int currXMax = MIN(99, currXLoc + 5);
    int currYMin = MAX(0, currYLoc - 5);
    int currYMax = MIN(99, currYLoc + 5);

    return (currXMin <= playerXLoc) && (playerXLoc <= currXMax) &&
            (currYMin <= playerYLoc) && (playerYLoc <= currYMax);
};

/*
 * Returns the direction the player moved in, based on current
 * statistics, and the old x-location and y-location of the player.
 */
int movementDirection(playerStatistics* self, int oldXLoc, int oldYLoc) {
    int currXLoc = self->xLoc;
    int currYLoc = self->yLoc;

    if (oldYLoc == currYLoc)
        if (((0 <= currXLoc) && (currXLoc < 3)) || ((oldXLoc + 3) == currXLoc))
            return EAST;
        else
            return WEST;
    else // oldXLoc == currXLoc
        if (((0 <= currYLoc) && (currYLoc < 3)) || ((oldYLoc + 3) == currYLoc))
        return SOUTH;
    else
        return NORTH;
};

/*
 * Returns a 1 if moving in the given direction causes the player to
 * exceed the boundaries of the server, and 0 otherwise.
 */
int willExceedBoundary(playerStatistics* self,
        int direction, int minX, int maxX, int minY, int maxY) {

    int currX = self->xLoc, currY = self->yLoc;

    switch (direction) {
        case NORTH:
            return 0;
            // If servers were divided across y, this should be
            // return ((currY - 3) < minY);
        case SOUTH:
            return 0;
            // If servers were divided across y, this should be
            // return ((currY + 3) > maxY);
        case WEST:
            return ((currX - 3) < minX);
        case EAST:
            return ((currX + 3) > maxX);
        default:
            return 0;
    }
};

/* 
 * Returns a 1 if the x-/y-boundary of the server area is visible,
 * and 0 otherwise. The xOrY toggles between determining if the x-boundary
 * is visible (1) or if the y-boundary is visible (0).
 */
int boundaryVisible(playerStatistics* self, int xOrY,
        int minX, int maxX, int minY, int maxY) {
    if (self == NULL) return NULL;

    int currXLoc = self->xLoc;
    int currYLoc = self->yLoc;

    int currXMin = MAX(minX, currXLoc - 5);
    int currXMax = MIN(maxX, currXLoc + 5);
    int currYMin = MAX(minY, currYLoc - 5);
    int currYMax = MIN(maxY, currYLoc + 5);

    return ((((currXMin <= minX) || (maxX <= currXMax)) && xOrY) ||
            (((currYMin <= minY) || (maxY <= currYMax)) && !xOrY)) ? 1 : 0;
};

/*
 * Processes a packet sent by the server, by looking at the
 * message type contained in `contentsReceived' and appropriately
 * updating the state of the relevant client: the self is `self',
 * and the player list is stored in `playerList'.
 */
void processServerPacket(packetContents* contentsReceived,
        playerStatistics* self, playerList* currentPlayers, int* loggedIn,
        int* minX, int* maxX, int* minY, int* maxY) {

    // Grab the necessary statistics from `contentsReceived'.
    playerStatistics statsRecvd;
    statsRecvd.hp = contentsReceived->hp;
    statsRecvd.exp = contentsReceived->exp;
    statsRecvd.xLoc = contentsReceived->xLoc;
    statsRecvd.yLoc = contentsReceived->yLoc;

    strncpy(statsRecvd.name, contentsReceived->victimName,
            EE122_MAX_NAME_LENGTH);
    statsRecvd.name[EE122_MAX_NAME_LENGTH] = 0;

    switch (contentsReceived->messageType) {
        case LOGIN_REPLY:
            /*
             * Set your own player statistics. LOGIN_REPLY only cares
             * about our own client, whereas MOVE_NOTIFY tells us who
             * to update by searching for the name.
             */
            if ((!(*loggedIn)) && contentsReceived->errorCode == 0)
                updateStats(self, &statsRecvd);

            if (!(*loggedIn)) // Suppress output if already logged in.
                on_login_reply(contentsReceived->errorCode);

            *loggedIn = 1;
            break;
        case MOVE_NOTIFY:
            /*
             * The algorithm is as follows:
             * Compare the victimName to self->name to see if it is
             *  the client itself.
             * Else, scan the linked list of players (currentPlayers)
             *  to see if we have a match.
             * If we have a match, then update the details of that
             *  node/player.
             * If we do not get a match, then add a new player to
             *  the list with the stored details.
             */

            // Is the moving player yourself?
            if (strcmp(contentsReceived->victimName, self->name) == 0) {
                updateStats(self, &statsRecvd);
                int currXLoc = self->xLoc;
                int currYLoc = self->yLoc;
                on_move_notify(contentsReceived->victimName, currXLoc,
                        currYLoc, self->hp, self->exp);

                int boundaryCheck;
                if ((boundaryCheck =
                        boundaryVisible(self, 1,
                        *minX, *maxX, *minY, *maxY)) != 0) // x-boundary check
                    on_close_to_boundary(1,
                        ((currXLoc <= (*minX + 5)) ? *minX : *maxX));

                if ((boundaryCheck =
                        boundaryVisible(self, 0,
                        *minX, *maxX, *minY, *maxY)) != 0) // y-boundary check
                    on_close_to_boundary(2,
                        ((currYLoc <= (*minY + 5)) ? *minY : *maxY));

            } else {
                addPlayer(currentPlayers, &statsRecvd, -1);
                if (withinRange(self, &statsRecvd)) {
                    on_move_notify(statsRecvd.name, statsRecvd.xLoc,
                            statsRecvd.yLoc, statsRecvd.hp,
                            statsRecvd.exp);
                }
            }
            break;
        case ATTACK_NOTIFY:
        {
            playerListNode* attackerNode;
            playerListNode* victimNode;

            attackerNode = accessPlayer(currentPlayers,
                    contentsReceived->attackerName);

            victimNode = accessPlayer(currentPlayers,
                    contentsReceived->victimName);

            playerStatistics* attackerStats = (attackerNode == NULL) ?
                    self : attackerNode->statistics;
            playerStatistics* victimStats = (victimNode == NULL) ?
                    self : victimNode->statistics;

            if ((withinRange(self, attackerStats) &&
                    withinRange(self, victimStats))) {
                on_attack_notify(contentsReceived->attackerName,
                        contentsReceived->victimName,
                        contentsReceived->damage, contentsReceived->hp);
            }
            break;
        }
        case SPEAK_NOTIFY:
            on_speak_notify(contentsReceived->victimName,
                    contentsReceived->message);
            break;
        case LOGOUT_NOTIFY:
            removePlayer(currentPlayers, contentsReceived->victimName);
            if (!strcmp(contentsReceived->victimName, self->name) == 0)
                on_exit_notify(contentsReceived->victimName);
            break;
        case INVALID_STATE:
            on_invalid_state(contentsReceived->errorCode);
            break;
    }
};

/*
 * Performs the necessary set of interactions when logging the client in.
 */
void logClientIn(int* tcpSocketFd, int udpSocketFd,
        char* trackerIp, int trackerPort,
        playerStatistics* self, UDP_Contents* udpContents, long int* globalId,
        playerList* currentPlayers,
        int* minX, int* maxX, int* minY, int* maxY,
        int relogin) {

    strncpy(udpContents->playerName, self->name, EE122_MAX_NAME_LENGTH);
    udpContents->playerName[EE122_MAX_NAME_LENGTH] = 0x00;

    char sendBuffer[MAX_BUFFER_SIZE], recvBuffer[MAX_BUFFER_SIZE];
    struct sockaddr_in address;
    socklen_t addressLength;
    int trackerIpAddr = inet_addr(trackerIp);
    int numBytesRecvd, numBytesSent;

    if (!relogin) { // First time log-in.
        udpContents->id = *globalId;
        // Increment the global ID for whichever function needs to use it next.
        *(globalId) = *(globalId) + 1;

        udpContents->messageType = STORAGE_LOCATION_REQUEST;
        strncpy(udpContents->playerName, self->name, EE122_MAX_NAME_LENGTH);
        udpContents->playerName[EE122_MAX_NAME_LENGTH] = 0x00;

        // Contact the tracker to obtain the server where state is stored.
        numBytesSent = assembleUdpPacket(sendBuffer, udpContents);
        address.sin_family = AF_INET;
        address.sin_addr.s_addr = trackerIpAddr;
        address.sin_port = htons(trackerPort);
        addressLength = sizeof (address);

        numBytesRecvd = sendAndRecvUdp(udpSocketFd, sendBuffer,
                numBytesSent, recvBuffer, &address, &addressLength,
                currentPlayers);

        int stateServerIp;
        short int stateServerUdpPort;

        if (numBytesRecvd < 12) {
            on_malformed_udp();
            return;
        } else if (address.sin_addr.s_addr != trackerIpAddr) {
            on_invalid_udp_source();
            return;
        } else {
            parseUdpPacket(recvBuffer, udpContents);

            if (udpContents->messageType != STORAGE_LOCATION_RESPONSE) {
                on_malformed_udp();
                return;
            } else if (udpContents->errorFlag == 1) {
                on_malformed_udp();
                return;
            }
        }

        stateServerIp = udpContents->server_ip_add;
        stateServerUdpPort = udpContents->server_udp_port;
        on_loc_resp(STORAGE_LOCATION_RESPONSE, stateServerIp,
                stateServerUdpPort);

        udpContents->messageType = PLAYER_STATE_REQUEST;
        udpContents->id = *(globalId);
        *(globalId) = *(globalId) + 1;

        // Contact state storage server to obtain state.
        numBytesSent = assembleUdpPacket(sendBuffer, udpContents);
        address.sin_family = AF_INET;
        address.sin_addr.s_addr = htonl(stateServerIp);
        address.sin_port = htons(stateServerUdpPort);
        addressLength = sizeof (address);

        numBytesRecvd =
                sendAndRecvUdp(udpSocketFd, sendBuffer, numBytesSent,
                recvBuffer, &address, &addressLength, currentPlayers);

        if (numBytesRecvd < 28) {
            on_malformed_udp();
            return;
        } else if (address.sin_addr.s_addr != htonl(stateServerIp)) {
            on_invalid_udp_source();
            return;
        } else {
            parseUdpPacket(recvBuffer, udpContents);

            if (udpContents->messageType != PLAYER_STATE_RESPONSE) {
                on_malformed_udp();
                return;
            } else if (udpContents->errorFlag == 1) {
                on_malformed_udp();
                return;
            }
        }

        // Store player state.
        self->hp = udpContents->hp;
        self->exp = udpContents->exp;
        self->xLoc = udpContents->xLoc;
        self->yLoc = udpContents->yLoc;
        on_state_resp(PLAYER_STATE_RESPONSE, udpContents->playerName,
                self->hp, self->exp, self->xLoc, self->yLoc);
    }

    // Contact the tracker to determine which server to log in to.
    udpContents->messageType = SERVER_AREA_REQUEST;
    udpContents->id = *(globalId);
    *(globalId) = *(globalId) + 1;

    numBytesSent = assembleUdpPacket(sendBuffer, udpContents);
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = trackerIpAddr;
    address.sin_port = htons(trackerPort);
    numBytesRecvd =
            sendAndRecvUdp(udpSocketFd, sendBuffer, numBytesSent,
            recvBuffer, &address, &addressLength, currentPlayers);

    if (numBytesRecvd < 16) {
        on_malformed_udp();
        return;
    } else if (address.sin_addr.s_addr != trackerIpAddr) {
        on_invalid_udp_source();
        return;
    } else {
        parseUdpPacket(recvBuffer, udpContents);

        if (udpContents->messageType != SERVER_AREA_RESPONSE) {
            on_malformed_udp();
            return;
        } else if (udpContents->errorFlag == 1) {
            on_malformed_udp();
            return;
        }
    }

    // Get file descriptor for connection to socket.
    *tcpSocketFd = socket(PF_INET, SOCK_STREAM, 0);
    if (*tcpSocketFd < 0) {
        // Unable to create connection
        fprintf(stdout, "!socket: Cannot create TCP socket on client. \n");
        done(currentPlayers, EXIT_FAILURE);
    }

    on_area_resp(SERVER_AREA_RESPONSE, udpContents->server_ip_add,
            udpContents->server_tcp_port, udpContents->minX,
            udpContents->maxX, 0, 99);

    // Forge a connection to server to receive TCP packets.
    struct sockaddr_in tcpSin;

    memset(&tcpSin, 0, sizeof (tcpSin));
    tcpSin.sin_family = AF_INET;
    tcpSin.sin_addr.s_addr = htonl(udpContents->server_ip_add);
    tcpSin.sin_port = htons(udpContents->server_tcp_port);
    if (connect(*tcpSocketFd, (struct sockaddr *) & tcpSin,
            sizeof (tcpSin)) < 0) {
        fprintf(stdout, "!connect: Cannot connect to server. \n");
        done(currentPlayers, EXIT_FAILURE);
    }

    // Set up the boundaries for the server.
    *minX = udpContents->minX;
    *minY = 0;
    *maxX = udpContents->maxX;
    *maxY = 99;
};

/*
 * Performs the necessary set of interactions when saving the state
 * of the client (such as when logging out or auto-saving).
 */
void saveClientState(int udpSocketFd,
        char* trackerIp, int trackerPort,
        playerStatistics* self, UDP_Contents* udpContents, long int* globalId,
        playerList* currentPlayers) {

    strncpy(udpContents->playerName, self->name, EE122_MAX_NAME_LENGTH);
    udpContents->playerName[EE122_MAX_NAME_LENGTH] = 0x00;

    char sendBuffer[MAX_BUFFER_SIZE], recvBuffer[MAX_BUFFER_SIZE];
    struct sockaddr_in address;
    socklen_t addressLength;
    int trackerIpAddr = inet_addr(trackerIp);
    int numBytesRecvd, numBytesSent;

    udpContents->id = *globalId;
    // Increment the global ID for whichever function needs to use it next.
    *(globalId) = *(globalId) + 1;

    udpContents->messageType = STORAGE_LOCATION_REQUEST;
    strncpy(udpContents->playerName, self->name, EE122_MAX_NAME_LENGTH);
    udpContents->playerName[EE122_MAX_NAME_LENGTH] = 0x00;

    // Contact the tracker to obtain the server where state is stored.
    numBytesSent = assembleUdpPacket(sendBuffer, udpContents);
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = trackerIpAddr;
    address.sin_port = htons(trackerPort);
    addressLength = sizeof (address);

    numBytesRecvd = sendAndRecvUdp(udpSocketFd, sendBuffer,
            numBytesSent, recvBuffer, &address, &addressLength,
            currentPlayers);

    int stateServerIp;
    short int stateServerUdpPort;

    if (numBytesRecvd < 12) {
        on_malformed_udp();
        return;
    } else if (address.sin_addr.s_addr != trackerIpAddr) {
        on_invalid_udp_source();
        return;
    } else {
        parseUdpPacket(recvBuffer, udpContents);

        if (udpContents->messageType != STORAGE_LOCATION_RESPONSE) {
            on_malformed_udp();
            return;
        } else if (udpContents->errorFlag == 1) {
            on_malformed_udp();
            return;
        }
    }

    stateServerIp = udpContents->server_ip_add;
    stateServerUdpPort = udpContents->server_udp_port;
    on_loc_resp(STORAGE_LOCATION_RESPONSE, stateServerIp,
            stateServerUdpPort);

    udpContents->messageType = SAVE_STATE_REQUEST;
    strncpy(udpContents->playerName, self->name, EE122_MAX_NAME_LENGTH);
    udpContents->playerName[EE122_MAX_NAME_LENGTH] = 0x00;

    udpContents->id = *(globalId);
    *(globalId) = *(globalId) + 1;
    udpContents->hp = self->hp;
    udpContents->exp = self->exp;
    udpContents->xLoc = self->xLoc;
    udpContents->yLoc = self->yLoc;

    // Contact state storage server to obtain state.
    numBytesSent = assembleUdpPacket(sendBuffer, udpContents);
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(stateServerIp);
    address.sin_port = htons(stateServerUdpPort);
    addressLength = sizeof (address);

    numBytesRecvd =
            sendAndRecvUdp(udpSocketFd, sendBuffer, numBytesSent,
            recvBuffer, &address, &addressLength, currentPlayers);

    if (numBytesRecvd < 8) {
        on_malformed_udp();
        return;
    } else if (address.sin_addr.s_addr != htonl(stateServerIp)) {
        on_invalid_udp_source();
        return;
    } else {
        parseUdpPacket(recvBuffer, udpContents);

        if (udpContents->messageType != SAVE_STATE_RESPONSE) {
            on_malformed_udp();
            return;
        } else if (udpContents->errorFlag == 1) {
            on_malformed_udp();
            return;
        }
    }

    on_save_resp(SAVE_STATE_RESPONSE, udpContents->errorCode);
};

/*
 * Deals with the sending and receiving of a UDP packet.
 * Attempts to be as reliable as possible; implements exponential backoff.
 */
int sendAndRecvUdp(int udpListener,
        char* udpBuffer, int msgLength, char* recvBuffer,
        struct sockaddr_in* address, socklen_t *addressLen,
        playerList* currentPlayers) {

    int attempt, recvReply = 0;

    // Set up file descriptor set.
    fd_set rfds;
    struct timeval tv;
    int recvLen;

    for (attempt = 1; attempt <= 4 && !recvReply; attempt++) {
        on_udp_attempt(attempt);
        sendto(udpListener, udpBuffer, msgLength, 0,
                (struct sockaddr *) address, *addressLen);

        FD_ZERO(&rfds);
        FD_SET(udpListener, &rfds);

        // Set timeout
        tv.tv_sec = 0;
        tv.tv_usec = 100000;
        tv.tv_usec = (tv.tv_usec << (attempt - 1));

        recvLen = select(udpListener + 1, &rfds, NULL, NULL, &tv);

        if (recvLen < 0) {
            fprintf(stdout, "!select: Error in receiving over UDP.");
            done(currentPlayers, EXIT_FAILURE);
            return -1;
        } else if (recvLen > 0) { // We have received something over UDP.
            recvLen = recvfrom(udpListener, recvBuffer, MAX_RECV_LEN,
                    0, (struct sockaddr *) address, addressLen);

            if (recvLen < 0) {
                fprintf(stdout, "!recvfrom: Error in receiving over UDP.");
                done(currentPlayers, EXIT_FAILURE);
                return -1;
            }

            recvReply = 1;
        }
    }

    if (recvReply == 0) { // Exponential backoff did not produce a reply.
        on_udp_fail();
        done(currentPlayers, EXIT_FAILURE);
        return -1;
    }

    return recvLen;
};

/*
 * Process a command supplied by the user.
 */
void processUserCommand(int* tcpSocketFd, int udpSocketFd,
        char* trackerIp, int trackerPort,
        packetContents* contentsToSend, UDP_Contents* udpContentsToSend,
        char* userInput,
        playerStatistics* self, playerList* currentPlayers,
        int* loggedIn, long int* globalId,
        int* minX, int* maxX, int* minY, int* maxY) {

    char userCommand[MAX_COMMAND_LEN + 1];

    // LOGIN command?
    strncpy(userCommand, userInput, 5);
    userCommand[5] = 0x00;
    toLower(userCommand);

    if (strcmp(userCommand, "login") == 0) {
        contentsToSend->messageType = LOGIN_REQUEST;
        char *victimName = contentsToSend->victimName;

        strncpy(victimName, userInput + 6, EE122_MAX_NAME_LENGTH);

        replaceNewline(victimName);
        // End name with a null terminator.
        victimName[EE122_MAX_NAME_LENGTH] = 0;

        // Update the name in self, and log client in,
        // only if not already logged in.
        if (!(*loggedIn)) {
            strncpy(self->name, victimName, EE122_MAX_NAME_LENGTH);
            self->name[EE122_MAX_NAME_LENGTH] = 0;

            logClientIn(tcpSocketFd, udpSocketFd, trackerIp, trackerPort, self,
                    udpContentsToSend, globalId, currentPlayers,
                    minX, maxX, minY, maxY, 0);
        }

        contentsToSend->hp = self->hp;
        contentsToSend->exp = self->exp;
        contentsToSend->xLoc = self->xLoc;
        contentsToSend->yLoc = self->yLoc;

        char bytesToSend[16];
        int numBytes = assemblePacket(bytesToSend, contentsToSend);
        sendBytes(*tcpSocketFd, bytesToSend, numBytes);
        show_prompt();
        return;
    }

    // If we have reached this point, this might mean that we are
    // processing a command when the client is not even logged in.
    if (!(*loggedIn)) {
        on_before_login();
        show_prompt();
        return;
    }

    // SPEAK command?
    if (strcmp(userCommand, "speak") == 0) {
        contentsToSend->messageType = SPEAK;

        char *message = contentsToSend->message;

        strncpy(message, userInput + 6, EE122_MAX_MSG_LENGTH);

        replaceNewline(message);
        // End name with a null terminator.
        message[EE122_MAX_MSG_LENGTH] = 0;

        char bytesToSend[MAX_SEND_LEN];
        int numBytes = assemblePacket(bytesToSend, contentsToSend);
        sendBytes(*tcpSocketFd, bytesToSend, numBytes);
        return;
    }

    // MOVE command?
    strncpy(userCommand, userInput, 4);
    userCommand[4] = 0x00;
    toLower(userCommand);

    if (strcmp(userCommand, "move") == 0) {
        contentsToSend->messageType = MOVE;

        char direction = tolower(userInput[5]);
        if (direction == 'n')
            contentsToSend->moveDir = NORTH;
        else if (direction == 's')
            contentsToSend->moveDir = SOUTH;
        else if (direction == 'w')
            contentsToSend->moveDir = WEST;
        else if (direction == 'e')
            contentsToSend->moveDir = EAST;

        if (!willExceedBoundary(self, contentsToSend->moveDir,
                *minX, *maxX, *minY, *maxY)) {
            char bytesToSend[8];
            int numBytes = assemblePacket(bytesToSend, contentsToSend);
            sendBytes(*tcpSocketFd, bytesToSend, numBytes);
            return;
        } else {
            // Get the new coordinates.
            int xLoc = self->xLoc, yLoc = self->yLoc;
            if (contentsToSend->moveDir == NORTH) {
                yLoc = yLoc - 3;
                if (yLoc < 0)
                    yLoc += 100;
                else if (yLoc > 99)
                    yLoc %= 100;
            } else if (contentsToSend->moveDir == SOUTH) {
                yLoc = yLoc + 3;
                if (yLoc < 0)
                    yLoc += 100;
                else if (yLoc > 99)
                    yLoc %= 100;
            } else if (contentsToSend->moveDir == WEST) {
                xLoc = xLoc - 3;
                if (xLoc < 0)
                    xLoc += 100;
                else if (xLoc > 99)
                    xLoc %= 100;
            } else if (contentsToSend->moveDir == EAST) {
                xLoc = xLoc + 3;
                if (xLoc < 0)
                    xLoc += 100;
                else if (xLoc > 99)
                    xLoc %= 100;
            }
            udpContentsToSend->xLoc = xLoc;
            udpContentsToSend->yLoc = yLoc;

            // Log out.
            contentsToSend->messageType = LOGOUT;
            *loggedIn = 0; // Change the value of the boolean variable.

            char bytesToSend[16];
            int numBytes = assemblePacket(bytesToSend, contentsToSend);
            sendBytes(*tcpSocketFd, bytesToSend, numBytes);
            close(*tcpSocketFd);

            // Now, log back in.
            contentsToSend->messageType = LOGIN_REQUEST;

            logClientIn(tcpSocketFd, udpSocketFd, trackerIp, trackerPort,
                    self, udpContentsToSend, globalId, currentPlayers,
                    minX, maxX, minY, maxY, 1);
            *loggedIn = 1;

            contentsToSend->hp = self->hp;
            contentsToSend->exp = self->exp;
            contentsToSend->xLoc = udpContentsToSend->xLoc;
            contentsToSend->yLoc = udpContentsToSend->yLoc;

            numBytes = assemblePacket(bytesToSend, contentsToSend);
            sendBytes(*tcpSocketFd, bytesToSend, numBytes);
            show_prompt();

            return;
        }
    }

    // ATTACK command?
    strncpy(userCommand, userInput, 6);
    userCommand[6] = 0x00;
    toLower(userCommand);

    if (strcmp(userCommand, "attack") == 0) {

        char opponentName[EE122_MAX_NAME_LENGTH + 1];
        strncpy(opponentName, userInput + 7, EE122_MAX_NAME_LENGTH);
        opponentName[EE122_MAX_NAME_LENGTH] = 0;
        replaceNewline(opponentName);

        playerListNode* opponentNode = accessPlayer(currentPlayers,
                opponentName);
        if (opponentNode == NULL) {
            on_not_visible();
            return;
        }
        playerStatistics* opponentStats = opponentNode->statistics;

        if ((strcmp(self->name, opponentName) == 0)) {
            fprintf(stdout, "No suicide allowed.");
            return;
        } else if (withinRange(self, opponentStats)) {
            strncpy(contentsToSend->victimName,
                    opponentName, EE122_MAX_NAME_LENGTH);
            contentsToSend->victimName[EE122_MAX_NAME_LENGTH] = 0;

            contentsToSend->messageType = ATTACK;

            char bytesToSend[12];
            int numBytes = assemblePacket(bytesToSend, contentsToSend);
            sendBytes(*tcpSocketFd, bytesToSend, numBytes);
            show_prompt();
            return;
        } else {
            on_not_visible();
            return;
        }
    }

    // LOGOUT command?
    strncpy(userCommand, userInput, 6);
    userCommand[6] = 0x00;
    toLower(userCommand);

    if (strcmp(userCommand, "logout") == 0) {
        contentsToSend->messageType = LOGOUT;

        *loggedIn = 0; // Change the value of the boolean variable.

        saveClientState(udpSocketFd, trackerIp, trackerPort, self,
                udpContentsToSend, globalId, currentPlayers);

        char bytesToSend[4];
        int numBytes = assemblePacket(bytesToSend, contentsToSend);
        sendBytes(*tcpSocketFd, bytesToSend, numBytes);
        return;
    }
};

int main(int argc, char** argv) {
    // Set up variables to be used throughout the execution of the program.
    playerList currentPlayers;
    currentPlayers.head = NULL;

    playerStatistics self; // Our own data, for efficiency.
    int loggedIn = 0; // Whether we are logged in.

    if (argc != 5) {
        // Not enough arguments.
        fprintf(stdout,
                "Expected command-line argument format: -s <Tracker IP> -u <Tracker port> \n");
        done(&currentPlayers, EXIT_FAILURE);
    }

    char *trackerIp;
    int trackerPort;

    // Parse command-line arguments.
    int i = 1;
    for (i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-s") == 0) {
            trackerIp = argv[i + 1];
            i++;
        } else if (strcmp(argv[i], "-p") == 0) {
            trackerPort = atoi(argv[i + 1]);
            i++;
        }
    }

    // Get UDP socket file descriptor.
    int tcpSocketFd = 0;
    int udpSocketFd = socket(PF_INET, SOCK_DGRAM, 0);
    if (udpSocketFd < 0) {
        // Unable to create connection.
        on_client_connect_failure();
        done(&currentPlayers, EXIT_FAILURE);
    }

    // Set up variables for infinite loop.
    fd_set readFds;

    // Buffer to hold received data.
    char buffer[MAX_BUFFER_SIZE];
    // Position in the buffer to insert the received data.
    int insertPosition = 0;

    // Struct to be converted into a TCP packet to be sent to the server.
    packetContents contentsToSend;
    // Struct holding data contained in TCP packet received from the server.
    packetContents contentsReceived;

    // Struct to be converted into a UDP packet to be sent to the server.
    UDP_Contents udpContentsToSend;

    // Initializing an ID globally to a random value between 0 to 999.
    unsigned long int GLOBAL_ID = rand() % 1000;

    // Boundaries for server that the client is currently logged into, if any.
    int minX, maxX, minY, maxY;

    // For extra credit: keep track of how many seconds until the next
    // autosave time.
    int howManySeconds = 60;
    time_t prev;
    time(&prev);

    show_prompt();

    // Infinite loop of reading from the server or standard input.
    while (1) {
        // Zero out the read file descriptor set.
        FD_ZERO(&readFds);

        // Fill in the read file descriptor set.
        FD_SET(STDIN, &readFds);
        FD_SET(tcpSocketFd, &readFds);

        // Set up timeout of server.
        struct timeval tv;
        tv.tv_sec = 60; // howManySeconds;
        tv.tv_usec = 0;

        int result = select(tcpSocketFd + 1, &readFds, NULL, NULL, &tv);
        if (result == -1) {
            if (loggedIn == 0) {
                // Connection has failed.
                close(tcpSocketFd);
                on_disconnection_from_server();
                done(&currentPlayers, EXIT_FAILURE);
            } else {
                // User logged out.
                close(tcpSocketFd);
                done(&currentPlayers, EXIT_SUCCESS);
            }
        } else if (result == 0) { // Extra credit. Perform auto-save.
            if (loggedIn) {
                saveClientState(udpSocketFd, trackerIp, trackerPort,
                        &self, &udpContentsToSend, &GLOBAL_ID, &currentPlayers);
                show_prompt();
            }

            // Reset the clock.
            // howManySeconds = 60;
            // time(&prev);
        } else if (result > 0) {
            // One of the file descriptors has data. Which one?

            if (FD_ISSET(STDIN, &readFds)) {
                // User has typed in information. Deal with it.
                char userInputBuffer[MAX_INPUT_LEN + 1];
                fgets(userInputBuffer, MAX_INPUT_LEN + 1, stdin);

                processUserCommand(&tcpSocketFd, udpSocketFd,
                        trackerIp, trackerPort,
                        &contentsToSend, &udpContentsToSend,
                        userInputBuffer,
                        &self, &currentPlayers, &loggedIn, &GLOBAL_ID,
                        &minX, &maxX, &minY, &maxY);
            } else if (FD_ISSET(tcpSocketFd, &readFds)) {
                // TCP socket to the server has data. Deal with it.
                int processingDone = 0;

                int recvdBytes = recv(tcpSocketFd,
                        buffer + insertPosition,
                        MAX_BUFFER_SIZE - insertPosition, 0);

                if (recvdBytes <= 0) {
                    // Connection has failed.
                    close(tcpSocketFd);
                    on_disconnection_from_server();
                    done(&currentPlayers, EXIT_FAILURE);
                }

                // Loop to process received packets.
                // Note that there may be more than one packet in the queue,
                // and sometimes there may even be less than one packet.
                while (!processingDone) {
                    insertPosition += recvdBytes;

                    if (insertPosition < 2) {
                        // Incomplete information about packet length.
                        processingDone = 1;
                    } else {
                        // Retrieve length of packet.
                        int length = (buffer[1] << 8);
                        length = length | buffer[2];

                        if (insertPosition < length) {
                            // We need to wait for the rest of the packet.
                            processingDone = 1;
                        } else {
                            parsePacket(buffer, &contentsReceived);
                            processServerPacket(&contentsReceived, &self,
                                    &currentPlayers,
                                    &loggedIn, &minX, &maxX, &minY, &maxY);

                            // We have processed one packet. Shift the rest of
                            // the buffer ahead.
                            int curr;
                            for (curr = length; curr < insertPosition; curr++) {
                                buffer[curr - length] = buffer[curr];
                            }
                            insertPosition -= length;
                            recvdBytes = 0;
                        }
                    }
                }
            }

            /* time_t curr;
            time(&curr);

            if ((howManySeconds = difftime(curr, prev)) >= 60)
                howManySeconds = 0; */
        }
    }

    close(udpSocketFd);
    close(tcpSocketFd);
    done(&currentPlayers, EXIT_SUCCESS);
};
